package br.com.intelsys.basics.imail;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.mail.Email;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.HtmlEmail;
import org.apache.commons.mail.SimpleEmail;
import org.apache.velocity.app.VelocityEngine;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.springframework.ui.velocity.VelocityEngineUtils;

/**
 * Eh possivel enviar e-mails simples, com textos puros (plain text), em formato
 * HTML e tambem e-mail com anexos.
 * 
 * Para o envio de e-mails estou usando a API do projeto Jakarta Apache, o
 * Commons-email.
 * 
 * Os dados do servidor SMTP deverao estar no configMail.properties
 * 
 * @author Patrick Givisiez
 * 
 */

public class IMail {
	private static final String AT = "@";
	public static final String CHARSET_UTF_8 = "UTF-8";
	public static final String CHARSET_ISO_8859_1 = "ISO-8859-1";

	private static Log log = LogFactory.getLog(IMail.class);
	private String hostName; // o servidor SMTP para envio do e-mail
	private List<Map<String, String>> toList; // Lista de destinatario
	private List<Map<String, String>> replyToList; // Lista de retorno
	private List<Map<String, String>> ccList; // lista de destinatario CC
	private List<Map<String, String>> bccList; // lista de destinatario BCC
	private String fromEmail; // Email do remetente
	private String fromName; // Nome do remetente
	private String subject; // assunto do e-mail
	private String content; // conteudo do e-mail TXT
	private String htmlContent; // conteudo do e-mail HTML
	private String xmlAddressBookFile = "imailAddressBook.xml"; // Local onde
																// estao as
																// configuracoes
																// (XML)
	private String charSet;
	private static boolean isLoaded = false; // Se carregou as configuracoes do
												// XML

	private static Map<String, String> mapHostname;
	private static Map<String, Map<String, String>> mapToAddress;
	private static Map<String, Map<String, String>> mapFromAddress;
	private static Map<String, Map<String, String>> mapCcAddress;
	private static Map<String, Map<String, String>> mapBccAddress;

	private VelocityEngine velocityEngine;
	private String templateRootPath;
	private String templateName;
	private Map<String, Object> keyList;

	private static final String FIELD_NAME = "name";
	private static final String FIELD_ADDRESS = "address";

	/**
	 * Envia o e-mail em modo assincrono em uma segunda Thread, nao bloqueando a
	 * aplicacao.<br/>
	 * <br/>
	 * No entanto, os erros nao sao capturados.
	 */
	public IMailAsynchSender sendEmailAsynch() {
		IMailAsynchSender iMailAsynchSender = new IMailAsynchSender(this);
		Thread t = new Thread(iMailAsynchSender);
		t.start();
		return iMailAsynchSender;
	}

	/**
	 * Envia o e-mail
	 */
	public void sendEmail() throws IMailException {
		String htmlCont = getHtmlContent();
		if (htmlCont != null && htmlCont.length() > 0) {
			try {
				// Cria o e-mail HTML
				HtmlEmail email = new HtmlEmail();
				loadToList(email);
				loadReplyToList(email);
				loadCcList(email);
				loadBccList(email);
				if (fromName != null) {
					if (charSet != null) {
						email.setFrom(fromEmail, fromName, charSet);
					} else {
						email.setFrom(fromEmail, fromName);
					}
				} else {
					email.setFrom(fromEmail);
				}
				email.setHostName(getHostName());
				email.setSubject(subject);

				// configura a mensagem para o formato HTML
				email.setHtmlMsg(htmlCont);
				if (charSet != null) {
					email.setCharset(charSet);
				}

				// configure uma mensagem alternativa caso o servidor nao
				// suporte HTML
				if (content != null && content.length() > 0) {
					email.setTextMsg(content);
				}

				// envia o e-mail
				email.send();
			} catch (EmailException e) {
				throw new IMailException(e);
			}
		} else {
			try {
				// Cria o e-mail sem formatacao
				SimpleEmail email = new SimpleEmail();
				loadToList(email);
				loadReplyToList(email);
				loadCcList(email);
				loadBccList(email);
				email.setHostName(getHostName());
				if (fromName != null) {
					if (charSet != null) {
						email.setFrom(fromEmail, fromName, charSet);
					} else {
						email.setFrom(fromEmail, fromName);
					}
				} else {
					email.setFrom(fromEmail);
				}
				email.setSubject(subject);

				// configura a mensagem sem formatacao
				email.setMsg(content);

				// envia o e-mail
				email.send();
			} catch (EmailException e) {
				throw new IMailException(e);
			}
		}
		if (log.isDebugEnabled()) {
			log.debug("@@ BEGIN: E-mail sent:");
			log.debug("From: " + fromName + " <" + fromEmail + ">");
			log.debug("Subject: " + subject);
			log.debug("=============");
			if (htmlCont != null) {
				log.debug(htmlCont);
			}
			log.debug("-------------");
			if (content != null) {
				log.debug(content);
			}
			log.debug("=============");
			log.debug("@@ END: E-mail sent.");
		}
	}

	/**
	 * Alimenta o objeto de e-mail com os enderecos.
	 * 
	 * @throws EmailException
	 */
	private void loadToList(Email email) throws EmailException {
		// Verifica se tem lista de e-mails de destinatarios se nao tiver manda
		// para um e-mail apenas.
		if (toList != null) {
			for (Map<String, String> m : toList) {
				if (m.get(FIELD_NAME) != null) {
					if (log.isDebugEnabled()) {
						log.debug("Adding to: " + m.get(FIELD_ADDRESS) + " (" + m.get(FIELD_NAME) + ")");
					}
					email.addTo((String) m.get(FIELD_ADDRESS), (String) m.get(FIELD_NAME));
				} else {
					if (log.isDebugEnabled()) {
						log.debug("Adding to: " + m.get(FIELD_ADDRESS));
					}
					email.addTo((String) m.get(FIELD_ADDRESS));
				}
			}
		}
	}

	/**
	 * Alimenta o objeto de e-mail com os enderecos.
	 * 
	 * @throws EmailException
	 */
	private void loadReplyToList(Email email) throws EmailException {
		// Verifica se tem lista de e-mails de destinatarios se nao tiver manda
		// para um e-mail apenas.
		if (replyToList != null) {
			for (Map<String, String> m : replyToList) {
				if (m.get(FIELD_NAME) != null) {
					if (log.isDebugEnabled()) {
						log.debug("Adding to: " + m.get(FIELD_NAME) + " <" + m.get(FIELD_ADDRESS) + ">");
					}
					email.addReplyTo((String) m.get(FIELD_ADDRESS), (String) m.get(FIELD_NAME));
				} else {
					if (log.isDebugEnabled()) {
						log.debug("Adding to: " + m.get(FIELD_ADDRESS));
					}
					email.addReplyTo((String) m.get(FIELD_ADDRESS));
				}
			}
		}
	}

	/**
	 * Alimenta o objeto de e-mail com os enderecos.
	 * 
	 * @throws EmailException
	 */
	private void loadCcList(Email email) throws EmailException {
		// Verifica se tem lista de e-mails de destinatarios se nao tiver manda
		// para um e-mail apenas.
		if (ccList != null) {
			for (Map<String, String> m : ccList) {
				if (m.get(FIELD_NAME) != null) {
					email.addCc((String) m.get(FIELD_ADDRESS), (String) m.get(FIELD_NAME));
				} else {
					email.addCc((String) m.get(FIELD_ADDRESS));
				}
			}
		}
	}

	/**
	 * Alimenta o objeto de e-mail com os enderecos.
	 * 
	 * @throws EmailException
	 */
	private void loadBccList(Email email) throws EmailException {
		// Verifica se tem lista de e-mails de destinatarios se nao tiver manda
		// para um e-mail apenas.
		if (bccList != null) {
			for (Map<String, String> m : bccList) {
				if (m.get(FIELD_NAME) != null) {
					email.addBcc((String) m.get(FIELD_ADDRESS), (String) m.get(FIELD_NAME));
				} else {
					email.addBcc((String) m.get(FIELD_ADDRESS));
				}
			}
		}
	}

	/**
	 * Retorna o servidor para envio do e-mail.
	 */
	private String getHostName() {
		String hostNameRet;
		if (hostName == null || hostName.length() == 0) {
			loadIMailConfig();
			if (mapHostname != null && mapHostname.size() > 0) {
				if (log.isDebugEnabled()) {
					log.debug("Loading first hostname from list");
				}
				hostNameRet = (String) mapHostname.get((String) mapHostname.keySet().iterator().next());
			} else {
				hostNameRet = hostName;
			}
		} else {
			if (mapHostname != null) {
				hostNameRet = (String) mapHostname.get(hostName);
			} else {
				hostNameRet = null;
			}
			if (hostNameRet == null || hostNameRet.length() == 0) {
				hostNameRet = hostName;
			}
		}
		if (log.isDebugEnabled()) {
			log.debug("Using hostname: " + hostNameRet);
		}
		return (hostNameRet);
	}

	/**
	 * Seta o servidor para envio do e-mail.
	 */
	public IMail hostName(String hostname) {
		hostName = hostname;
		return this;
	}

	private Map<String, String> loadMap(String email, String name) {
		Map<String, String> emailMap = new HashMap<String, String>(1);
		emailMap.put(FIELD_ADDRESS, email);
		emailMap.put(FIELD_NAME, name);
		return (emailMap);
	}

	/**
	 * Adiciona um nome e e-mail para a mensagme ser enviada
	 * 
	 */
	public IMail addTo(String email, String name) {
		if (toList == null) {
			toList = new ArrayList<Map<String, String>>();
		}
		toList.add(loadMap(email, name));
		return this;
	}

	/**
	 * Adiciona um endereco e-mail para a mensagem ser enviada
	 * 
	 * Caso seja informado um valor sem "@" tentarei localizar um endereco com
	 * este nome em meu catalogo.
	 */
	public IMail addTo(String email) {
		if (email.contains(AT)) {
			addTo(email, null);
		} else {
			loadIMailConfig();
			if (mapToAddress != null && mapToAddress.containsKey(email)) {
				addTo((String) mapToAddress.get(email).get(FIELD_ADDRESS), (String) mapToAddress.get(email).get(FIELD_ADDRESS));
			}
		}
		return this;
	}

	/**
	 * Limpa toda a lista de destinatairio
	 * 
	 * @return
	 */
	public IMail clearTo() {
		toList.clear();
		return this;
	}

	/**
	 * Adiciona um nome e e-mail para a mensagme ser respondida
	 * 
	 */
	public IMail addReplyTo(String email, String name) {
		if (replyToList == null) {
			replyToList = new ArrayList<Map<String, String>>();
		}
		replyToList.add(loadMap(email, name));
		key("tplReplyToEmail", email);
		key("tplReplyToName", name);
		return this;
	}

	/**
	 * Adiciona um endereco e-mail para a mensagem ser respondida
	 * 
	 * Caso seja informado um valor sem "@" tentarei localizar um endereco com
	 * este nome em meu catalogo.
	 */
	public IMail addReplyTo(String email) {
		if (email.contains(AT)) {
			addReplyTo(email, null);
		} else {
			loadIMailConfig();
			if (mapToAddress != null && mapToAddress.containsKey(email)) {
				addReplyTo((String) mapToAddress.get(email).get(FIELD_ADDRESS), (String) mapToAddress.get(email).get(FIELD_ADDRESS));
			}
		}
		return this;
	}

	/**
	 * Adiciona um nome e e-mail para copia da mensagme a ser enviada
	 * 
	 */
	public IMail addCc(String email, String name) {
		if (ccList == null) {
			ccList = new ArrayList<Map<String, String>>();
		}
		ccList.add(loadMap(email, name));
		return this;
	}

	/**
	 * Adiciona um endereco de e-mail para copia da mensagme a ser enviada
	 * 
	 * Caso seja informado um valor sem "@" tentarei localizar um endereco com
	 * este nome em meu catalogo.
	 */
	public IMail addCc(String email) {
		if (email.contains(AT)) {
			addCc(email, null);
		} else {
			loadIMailConfig();
			if (mapToAddress != null && mapToAddress.containsKey(email)) {
				addCc((String) mapToAddress.get(email).get(FIELD_ADDRESS), (String) mapToAddress.get(email).get(FIELD_ADDRESS));
			}
		}
		return this;
	}

	/**
	 * Adiciona um nome e e-mail para copia oculta da mensagme a ser enviada
	 * 
	 */
	public IMail addBcc(String email, String name) {
		if (bccList == null) {
			bccList = new ArrayList<Map<String, String>>();
		}
		bccList.add(loadMap(email, name));
		return this;
	}

	/**
	 * Adiciona um endereco de e-mail para copia oculta da mensagme a ser
	 * enviada
	 * 
	 * Caso seja informado um valor sem "@" tentarei localizar um endereco com
	 * este nome em meu catalogo.
	 */
	public IMail addBcc(String email) {
		if (email.contains(AT)) {
			addBcc(email, null);
		} else {
			loadIMailConfig();
			if (mapToAddress != null && mapToAddress.containsKey(email)) {
				addBcc((String) mapToAddress.get(email).get(FIELD_ADDRESS), (String) mapToAddress.get(email).get(FIELD_ADDRESS));
			}
		}
		return this;
	}

	/**
	 * Define o e-mail e o nome do Remetente .
	 */
	public IMail from(String email, String name) {
		fromEmail = email;
		fromName = name;
		return this;
	}

	/**
	 * Define o e-mail do Remetente.
	 * 
	 * Caso seja informado um valor sem "@" tentarei localizar um endereco com
	 * este nome em meu catalogo.
	 */
	public IMail from(String email) {
		if (email.contains(AT)) {
			from(email, null);
		} else {
			loadIMailConfig();
			if (mapFromAddress != null && mapFromAddress.containsKey(email)) {
				fromEmail = (String) mapFromAddress.get(email).get(FIELD_ADDRESS);
				fromName = (String) mapFromAddress.get(email).get(FIELD_NAME);
				if (log.isDebugEnabled()) {
					log.debug("Setting from [" + email + "]: " + fromName + " <" + fromEmail + ">");
				}
			}
		}
		return this;
	}

	/**
	 * Define o assunto do email.
	 */
	public IMail subject(String subject) {
		this.subject = subject;
		return this;
	}

	/**
	 * Define o conteudo em "plain text" da mensagem.
	 */
	public IMail content(String content) {
		this.content = content;
		return this;
	}

	/**
	 * Retorna o conteudm em HTML do email.
	 */
	private String getHtmlContent() {
		if (htmlContent == null || htmlContent.length() == 0) {
			loadVelocityTemplate();
		}
		return htmlContent;
	}

	/**
	 * Define o conteudo em HTML do email.
	 */
	public IMail htmlContent(String htmlContent) {
		this.htmlContent = htmlContent;
		return this;
	}

	/**
	 * Define o caminho onde o sistema buscara o XML com o catalogo de
	 * enderecos.
	 */
	public void setXmlAddressBookFile(String addressBookFile) {
		xmlAddressBookFile = addressBookFile;
	}

	/**
	 * Define o Char Set do conteudo do e-mail.
	 * 
	 * @param charSet
	 */
	public void setCharSet(String charSet) {
		this.charSet = charSet;
	}

	/**
	 * Procura pelos arquivos de configuracao
	 * 
	 * @param file
	 *            Arquivo que ira ser procurado
	 * @return instancia do arquivo
	 */
	private File searchFileConf(String file) {
		String mainXml = null;
		try {
			Enumeration<URL> e = getClass().getClassLoader().getResources(file);
			while (e.hasMoreElements()) {
				mainXml = e.nextElement().getPath();
			}
		} catch (IOException e) {
			log.error("Nao foi possivel encontrar o arquivo de configuracao " + file + ". Verifique se ele esta em seu classpath.");
			e.printStackTrace();
		}
		if (mainXml == null) {
			File fileTmp = new File(file);
			if (fileTmp.exists()) {
				mainXml = file;
			} else {
				log.error("Nao foi possivel encontrar o arquivo de configuracao " + file + ". Verifique se ele esta em seu classpath.");
			}
		}
		return ((mainXml != null ? new File(mainXml) : null));
	}

	/**
	 * Carrega as configuracoes do IMail
	 */
	private void loadIMailConfig() {
		String fileName = null;
		try {
			if (!isLoaded) {
				File fileTmp = searchFileConf(xmlAddressBookFile);
				if (fileTmp != null) {
					fileName = fileTmp.getName();
					log.info("Configuracao " + xmlAddressBookFile + " encontrada.");
					readFactoryManager((new SAXBuilder()).build(fileTmp));
					isLoaded = true;
				}
			}
		} catch (NullPointerException e) {
			e.printStackTrace();
			log.error("Nao foi possivel acessar o arquivo de configuracao de email " + fileName
					+ ". Verifique se ele esta em seu classpath");
		} catch (IOException e) {
			e.printStackTrace();
			log.error("Nao foi possivel acessar o arquivo de configuracao de email " + fileName
					+ ". Verifique se ele esta em seu classpath");
		} catch (JDOMException e) {
			e.printStackTrace();
			log.error("Existe algum erro no arquivo de configuracao de email " + fileName);
		}
	}

	/**
	 * Faz o parse do arquivo de configuracao do IMail
	 * 
	 * @param mainDoc
	 *            SAX XML Document
	 */
	@SuppressWarnings("unchecked")
	private void readFactoryManager(Document mainDoc) {
		final String TMP_HOSTNAMELIST = "hostnameList";
		final String TMP_SMTP = "smtp";
		final String TMP_TOADDRESSLIST = "toAddressList";
		final String TMP_FROMADDRESSLIST = "fromAddressList";
		final String TMP_ALIAS = "alias";
		final String TMP_CCADDRESSLIST = "ccAddressList";
		final String TMP_BCCADDRESSLIST = "bccAddressList";

		Element eChild, eChild2;
		List<Element> lChild2;
		Iterator<Element> i, i2;
		Element imail = mainDoc.getRootElement();
		List<Element> elementList = imail.getChildren();
		i = elementList.iterator();

		mapFromAddress = new HashMap<String, Map<String, String>>();
		mapHostname = new HashMap<String, String>();
		mapToAddress = new HashMap<String, Map<String, String>>();
		mapCcAddress = new HashMap<String, Map<String, String>>();
		mapBccAddress = new HashMap<String, Map<String, String>>();

		log.info("Parsing config ...");
		while (i.hasNext()) {
			eChild = i.next();
			if (eChild.getName().equals(TMP_HOSTNAMELIST)) {
				lChild2 = eChild.getChildren();
				i2 = lChild2.iterator();
				while (i2.hasNext()) {
					eChild2 = i2.next();
					if (eChild2.getName().equals(TMP_SMTP)) {
						mapHostname.put(eChild2.getAttributeValue(FIELD_NAME), eChild2.getText());
					}
				}
			} else if (eChild.getName().equals(TMP_TOADDRESSLIST)) {
				lChild2 = eChild.getChildren();
				i2 = lChild2.iterator();
				while (i2.hasNext()) {
					eChild2 = i2.next();
					if (eChild2.getName().equals(FIELD_ADDRESS)) {
						Map<String, String> addr = new HashMap<String, String>(2);
						addr.put(FIELD_NAME, eChild2.getAttributeValue(FIELD_NAME));
						addr.put(FIELD_ADDRESS, eChild2.getText());
						mapToAddress.put(eChild2.getAttributeValue(TMP_ALIAS), addr);
					}
				}
			} else if (eChild.getName().equals(TMP_FROMADDRESSLIST)) {
				lChild2 = eChild.getChildren();
				i2 = lChild2.iterator();
				while (i2.hasNext()) {
					eChild2 = i2.next();
					if (eChild2.getName().equals(FIELD_ADDRESS)) {
						Map<String, String> addr = new HashMap<String, String>(2);
						addr.put(FIELD_NAME, eChild2.getAttributeValue(FIELD_NAME));
						addr.put(FIELD_ADDRESS, eChild2.getText());
						mapFromAddress.put(eChild2.getAttributeValue(TMP_ALIAS), addr);
					}
				}
			} else if (eChild.getName().equals(TMP_CCADDRESSLIST)) {
				lChild2 = eChild.getChildren();
				i2 = lChild2.iterator();
				while (i2.hasNext()) {
					eChild2 = i2.next();
					if (eChild2.getName().equals(FIELD_ADDRESS)) {
						Map<String, String> addr = new HashMap<String, String>(2);
						addr.put(FIELD_NAME, eChild2.getAttributeValue(FIELD_NAME));
						addr.put(FIELD_ADDRESS, eChild2.getText());
						mapCcAddress.put(eChild2.getAttributeValue(TMP_ALIAS), addr);
					}
				}
			} else if (eChild.getName().equals(TMP_BCCADDRESSLIST)) {
				lChild2 = eChild.getChildren();
				i2 = lChild2.iterator();
				while (i2.hasNext()) {
					eChild2 = i2.next();
					if (eChild2.getName().equals(FIELD_ADDRESS)) {
						Map<String, String> addr = new HashMap<String, String>(2);
						addr.put(FIELD_NAME, eChild2.getAttributeValue(FIELD_NAME));
						addr.put(FIELD_ADDRESS, eChild2.getText());
						mapBccAddress.put(eChild2.getAttributeValue(TMP_ALIAS), addr);
					}
				}
			}
		}
	}

	// ///////////// TEMPLATES
	/**
	 * Define a engine de templates Velocity. Este metodo eh normalmente usado
	 * pelo Spring
	 * 
	 * @param Velocity
	 *            Engine
	 */
	public void setVelocityEngine(VelocityEngine velocityEngine) {
		this.velocityEngine = velocityEngine;
	}

	/**
	 * Define o diretorio onde estarao os templates Este metodo eh normalmente
	 * usado pelo Spring
	 * 
	 * @param path
	 *            Diretorio no classpath onde estao os templates
	 */
	public void setTemplateRootPath(String path) {
		templateRootPath = path;
	}

	/**
	 * Nome do template que sera carregado. Ele se encontra no diretorio
	 * definido por setTemplateRootPath();
	 * 
	 * @param name
	 *            Nome do template que sera carregado
	 */
	public IMail templateName(String name) {
		templateName = name;
		return this;
	}

	/**
	 * Define as chaves para serem usadas no template. La estas chaves serao
	 * alteradas pelo valor definido
	 * 
	 * @param name
	 *            Nome da chave
	 * @param value
	 *            Valor que sera inserido no template
	 */
	public IMail key(String name, Object value) {
		if (keyList == null) {
			keyList = new HashMap<String, Object>();
		}
		keyList.put(name, value);
		return this;
	}

	/**
	 * Alias para definir uma chave
	 * 
	 * @param name
	 *            Nome da chave
	 * @param value
	 *            Valor que sera inserido no template
	 */
	public IMail text(String name, Object value) {
		return key(name, value);
	}

	/**
	 * Carrega o Velocity Engine e define um template de HTML
	 * 
	 */
	private void loadVelocityTemplate() {
		if (templateName != null && templateName.length() > 0) {
			key("tplSubject", subject);
			key("tplFromEmail", fromEmail);
			key("tplFromName", fromName);
			htmlContent(VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, templateRootPath + templateName, keyList));
		}
	}
}